[UE]CharacterMovement源码浅析
CharacterMovement源码浅析
Base
classDiagram
direction LR
class UMovementComponent {
UpdatedComponent : TObjectPtr~USceneComponent~ (处理空间位置)
UpdatedPrimitive : TObjectPtr~UPrimitiveComponent~ (处理渲染物理)
}
%% -------------
UMovementComponent<|--UProjectileMovementComponent
class UProjectileMovementComponent {
支持发射体(子弹等)
}
%% -------------
UMovementComponent<|--UNavMovementComponent
class UNavMovementComponent {
支持 Agent 寻路
NavAgentProps : FNavAgentProperties
}
UNavMovementComponent<|--UPawnMovementComponent
class UPawnMovementComponent {
支持输入控制
AddInputVector()
}
UPawnMovementComponent<|--UCharacterMovementComponent
Move 一般是先进行基础运动(PerformMovent),然后处理基于物理的模拟(Collision、Simulation);
flowchart LR UCharacterMovementComponent TickComponent -->ConsumeInputVector TickComponent -->ControlledCharacterMove TickComponent -->Other...
ComsumeInputVector:
从 PawnOwner 取出累积的 ControlInputVector,该值监听输入并调用 Pawn::AddMovementInput 得来;
ControlledCharacterMove:
进行 Character 移动的输入处理、物理模拟、同步;
Other...
ControlledCharacterMove
1 | void UCharacterMovementComponent::ControlledCharacterMove(const FVector& InputVector, float DeltaSeconds) |
flowchart LR ControlledCharacterMove -->CheckJumpInput -.->ScaleInputAcceleration -.->ComputeAnalogInputModifier ControlledCharacterMove -->|ROLE_Authority|PerformMovement -->Start(StartNewPhysics) ControlledCharacterMove -->|ROLE_AutonomousProxy & IsClient|ReplicateMoveToServer Start-->MOVE_None Start-->MOVE_Walking Start-->MOVE_Falling Start-->MOVE_Flying Start-->MOVE_Swimming Start-->MOVE_Custom
Input
解析输入相关的数据;
CheckJumpInput:根据bPressedJump,计算JumpCurrentCount、JumpForceTimeRemaining;ScaleInputAcceleration:根据玩家的输入InputVector,计算出当前的初始加速度值;ComputeAnalogInputModifier:模拟输入修正值,将Acceleration /= MaxAcceleration,限制在0-1内;
PerfomeMovement
进行基础运动模拟,设置位移。
Kinetic : Walking
flowchart LR UCharacterMovementComponent %%------------------------------------- PerformMovement -->StartNewPhysics -->PhysWalking %%------------------------------------- PhysWalking -->GetSimulationTimeStep PhysWalking -->CalcVelocity PhysWalking -->MoveAlongFloor PhysWalking -->FindFloor PhysWalking -->CheckLedges PhysWalking -->MaintainHorizontalGroundVelocity %%------------------------------------- MoveAlongFloor -->ComputeGroundMovementDelta MoveAlongFloor -->SafeMoveUpdatedComponent %%------------------------------------- CheckLedges -->|true|GetLedgeMove GetLedgeMove -->|true|RevertMove -->TryLedgeMove GetLedgeMove -->|false|bMustJump -->|true|RevertMove -->Fall CheckLedges -->|false|FloorCheck %%------------------------------------- FloorCheck -->IsWalkableFloor IsWalkableFloor -->|true|AdjustFloorHeight -->SetBase IsWalkableFloor -->|false|GetPenetrationAdjustment -->ResolvePenetration %%-------------------------------------
GetSimulationTimeStep:将 TickDeltaTime 按照 MaxSimulationTimeStep 分割为若干段(为了保证平滑),处理每一段的信息;
CalcVelocity:根据 Friction、bFluid、BrakingDeceleration 修改 Acceleration,并计算出 Velocity 水平速度;
MoveAlongFloor:根据 MoveVelocity 信息,先调用 ComputeGroundMovementDelta,根据 Velocity 计算出 RampVector 平行于斜面的移动距离,同时根据 bMaintainHorizontalGroundVelocity 处理沿斜面速度减慢的情况;然后调用 SafeMoveUpdatedComponent,进行 MoveUpdatedComponent,更新位置;在 UPrimitiveComponent::MoveComponentImpl 中,还会进行 World->ComponentSweepMulti 判断是否遇到障碍物;
FindFloor:更新 CurrentFloor : FFindFloorResult 信息;用 CharacterOwner->GetCapsuleComponent() 进行 FloorSweepTest,计算出 ValidPerchRadius 等信息;
CheckLedges:检测是否在 Ledge 附近,如果是,则先尝试寻找新的移动方向(通过 GetLedgeMove 进行 SweepSingleByChannel 计算出边缘法线返回新的反向);如果找不到新的方向,则尝试跳跃,检测 bMustJump,如果不能跳跃则取消移动;
FloorCheck:校验 Floor 相关数据,如果 Character 处于IsWalkableFloor 的 Floor,AdjustFloorHeight 来调整 Character 的高度,如果在 Floor 中则 GetPenetrationAdjustment 计算需要弹出 Character 的距离并 ResolvePenetration,防止 Floor 和 Character 有冲突卡住;
MaintainHorizontalGroundVelocity:调用之前判断是否依然 IsMovingOnGround,如果是则根据 bMaintainHorizontalGroundVelocity,计算 GravityRelativeVelocity 进而更新 Velocity;
Kinetic : Falling
flowchart LR PerformMovement -->StartNewPhysics -->PhysFalling PhysFalling -->GetFallingLateralAcceleration PhysFalling -->ShouldLimitAirControl PhysFalling -->RestorePreAdditiveRootMotionVelocity PhysFalling -->CalcVelocity PhysFalling -->ApplyRootMotionToVelocity PhysFalling -->NotifyJumpApex PhysFalling -->SafeMoveUpdatedComponent PhysFalling -->|IsSwimming|StartSwimming PhysFalling -->BlockingHit %%--------------------- BlockingHit -->|IsValidLandingPoint|ProcessLanded BlockingHit -->HandleImpact %%--------------------- ProcessLanded -->|IsFalling|SetPostLandedPhysics ProcessLanded -->StartNewPhysics_2 %%--------------------- HandleImpact -.-> CalcVelocity_2 -.-> BlockingHit_2
GetFallingLateralAcceleration:计算 Character 在水平方向上的加速度;重点是将 WorldAcceleration 通过RotateWorldToGravity 转为 Gravity 相关坐标系,然后将 Z 的方向设为 0,再转回 World 坐标系,这样以移除垂直方向上的加速度(因为垂直方向的加速度需要由 Gravity 决定,而不是 InputVector);
RestorePreAdditiveRootMotionVelocity:Apply AdditiveRootMotion 的情况下将 Velocity 设置为 LastPreAdditiveVelocity( AdditiveRootMotion 表示 RootMotioinVelocity 将与 Character 的原始速度 LastPreAdditiveVelocity,即计算 RootMotionVelocity 前的速度叠加),防止 RootMotion Velocity 被累加;
CalcVelocity:根据 FallAcceleration、Gravity、JumpForce 等数据,计算出 NewFallVelocity;
ApplyRootMotionToVelocity:应用 RootMotion Velocity,根据 HasOverrideVelocity / HasAdditiveVelocity 两种应用速度方式,计算 Velocity;
NotifyJumpApex:当 RotateWorldToGravity(Velocity).Z < 0 时,说明到达了 JumpApex 跳跃顶点,进行通知;
SafeMoveUpdatedComponent:进行位移设置;
BlokingHit:在碰到障碍物时的处理;
ProcessLanded:判断是否 IsValidLandingPoint,如果是,进行着陆;进行通知 Landed 与设置相关物理状态 SetPostLandedPhysics,然后开始新的物理模拟 StartNewPhysics;
HandleImpact:无法着陆时,AddImpactPhysicsForces ,用于后续计算碰撞后的 Velocity与位移;
BlockingHit_2:碰撞移动后再次计算是否再次 BlockingHit,如果无 Hit,则尝试 FindFloor,找到 Floor则尝试着陆;如果是,说明 Character 被卡在了两个障碍物中间;检测是否 IsValidLandingPoint 是着陆点,是则 ProcessLand;如果不是着陆点,特殊处理被卡住 bDitch 的情况(检查 OldHitImpactNormal、Hit.ImpactNormal 是否都具有 Z 即斜坡朝上,且夹角 >90° 即斜坡朝向不同,同时 Character 的 Delta.Z 接近 0 即在垂直方向无移动),如果是,尝试增加 Velocity 与位移,摆脱被卡住的情况;
Kinetic : Other
TODO…
ReplicateMoveToServer
对于 AutonomousProxy Character 将移动同步到服务器,同时进行 Client 本地的预表现;
主要维护三种 Move 数据:
Old Move:当前还未被DSACK的Move数据中,最早的一次Move;New Move:本次执行(即Client进行Perform)的Move;Pending Move:若某次New Move还未进行同步(等待并包),将其存储在Pending Move中,等待下次同步带上该数据;
在 CallServerMovePacked 时,打包三种 Move 同步;
首先需要了解:FNetworkPredictionData;
FNetworkPredictionData
PredictionData_Client_Character 维护 Client 的 Move 相关数据,同时用于合并、丢弃、比较、标记更新等操作;
classDiagram
class FNetworkPredictionData_Client_Character {
SavedMoves : TArray~FSavedMovePtr~
FreeMoves : TArray~FSavedMovePtr~
PendingMove : FSavedMovePtr
LastAckedMove : FSavedMovePtr
ClientUpdateRealTime : float
bUpdatePosition : uint32
...
}
FNetworkPredictionData_Client_Character-->FSavedMove_Character
class FSavedMove_Character {
TimeStamp : float
DeltaTime : float
Acceleration : FVector
MaxSpeed : float
Start / End / Saved : Location / ReletiveLocation / Rotation / Velocity / Floor
/ CapsuleRadius / CapsuleHalfHeight / Base / ActorOverlapCounter ...
...
}
其中:
FSavedMovePtr 是 TSharedPtr<FSavedMove_Character>;
SavedMoves 保存 Client 执行的 Move,在 CleintAck 后, LastAckedMove 将会被 Free 并从 SavedMoves 中移除;
PendingMove 记录 Client 最新执行的,还未 CallServer 的 Move,每次 Push 到 SavedMoves 中,同时可能会作为 OldMove 被 Combine;
FreeMoves 记录已经被标记 Free 的 Move,后续释放;
classDiagram
class FNetworkPredictionData_Server_Character {
PendingAdjustment : FClientAdjustment
}
FNetworkPredictionData_Server_Character-->FClientAdjustment
class FClientAdjustment {
TimeStamp : float
DeltaTime : float
bAckGoodMove : bool
New Loc / Vel / Rot / Base ...
...
}
PredictionData_Server_Character 记录在 Server 上的 Move 数据,用于校验、修正等;
其中:
PendingAdjustment 维护了一系列 ClientAdjust 所需的数据;
ReplicateMoveToServer - Logic
flowchart LR ReplicateMoveToServer -->GetPredictionData_Client_Character ReplicateMoveToServer -->ClientData_UpdateTimeStampAndDeltaTime ReplicateMoveToServer -->FindImportantMove ReplicateMoveToServer -->ClientData_CreateSavedMove -.->Move_SetMoveFor -.->Move_Combine ReplicateMoveToServer -->PerformMovement -.->Move_PostUpdate -.->ClientData_SaveMove ReplicateMoveToServer -->CallServerMove -.->ClearPending
GetPredictionData_Client_Character:获取 Client 的预测数据 ClientData : FNetworkPredictionData_Client_Character*;
ClientData 会在各个地方被更新,比如
ReplicateMoveToServer中:
更新物理模拟的TimeStamp与DeltaTime:ClientData->UpdateTimeStampAndDeltaTime;
创建新的SavedMove:ClientData->CreateSavedMove();
PerformMovement之后更新Location、Rotation、Velocity等数据:NewMove->PostUpdate(CharacterOwner, FSavedMove_Character::PostUpdate_Record);CallServerMove / CallServerMovePacked前更新时间:ClientData->ClientUpdateRealTime = MyWorld->GetRealTimeSeconds();ClientAckGoodMove:Client收到Server的移动确认时,更新最后的移动ClientData->LastAckedMove:
FindImportantMove:找到最早的未 Ack 的 ImportantMove 数据,IsImportantMove 指与上一个 Ack 的移动有差异的移动;判定是否 Important 时,会检查 CompressedFlags (压缩了 FLAG_JumpPressed、FLAG_WantsToCrouch 等信息)、Start/End PackedMovementMode、Acceleration 的大小、方向差异是否超过阈值;找到 Unack ImportMove 后,存储在 OldMove 中,后续将其与新的 Move 一起 CallServerMove,确保 Server 可以正确处理。
CreateSavedMove:创建新的 FSavedMove 数据,也就是定义一个新的 Move;
Move_SetMoveFor:根据 CharacterOwner、DeltaTime、NewAcceleration 等数据设置 Move 基本信息;
Move_Combine:尝试将这个新的 Move 与 待处理的移动 PendingMove 合并,如果 CanCombine,更新 Rotation、Position 等信息;CanCombine 会校验 TimeStamp、RootMotion、Acceleration、StartVelocity、MaxSpeed、Jump、CompressedFlags、MovementMode、StartCapsule Radius/HalfHeight、AttachParent、TimeDilation、ActorOverlapCounter 这些数据;Combine 时更新 Location、Rotation、Velocity、Floor、Jump 等数据;
PerformMovement:在本地执行移动;
Move_PostUpdate:在 PerformMovement 更新了移动相关数据之后,设置这些状态数据到 Move 中;
ClientData_SaveMove:将 NewMove 保存到移动列表 ClientData->SavedMoves 中;
CallServerMove:根据角色是否正在复制移动 bSendServerMove 将新的移动发动到 Server,根据 ShouldUsePackedMovementRPCs 决定发送的方式 CallServerMovePacked / CallServerMove;
ClearPendingMove:清空 PendingMove,表示没有待处理的移动;
AutonomousProxy
从 ReplicateMoveToServer -> CallServerMovePacked 继续出发:
flowchart LR CallServerMovePacked -->ServerMovePacked_ClientSend -->|DS|ServerMovePacked_Implementation -->ServerMovePacked_ServerReceive ServerMovePacked_ServerReceive -->ServerMove_HandleMoveData ServerMove_HandleMoveData -->SetCurrentNetworkMoveData ServerMove_HandleMoveData -->ServerMove_PerformMovement ServerMove_PerformMovement -->MoveAutonomous ServerMove_PerformMovement -->ServerMoveHandleClientError -->ServerCheckClientError MoveAutonomous -->PerformMovement
通过 CallServerMovedPacked (UnreliableRPC) 将打包的 SaveMoves 数据发送到 DS,DS 根据 Client 发送到的数据应用 Move 数据,进行SetCurrentNetworkMoveData、ServerMove_PerformMovent,同时在 ServerMove_PerformMovent 中 MoveAutonomous (内部还是 PerformMovement)与 校验数据合法性 CheckClientError;如果数据差异过大,则 ServerData->PendingAdjustment.bAckGoodMove = false;
Move 数据变化后,通过 Replicate 将其复制到 Client,主要涉及的数据有:
bReplicateMovement:标记是否要进行Move的同步;ReplicatedMovement:移动数据;ReplicatedBasedMovement:Base的移动数据;ReplicatedMovementMode:移动模式(Walk、Fall等)- 其它数据:
Transform、RootMotion等;
flowchart LR UNetDriver::TickFlush -->UNetDriver::ServerReplicateActors -->SendClientAdjustment SendClientAdjustment -->bAckGoodMove bAckGoodMove -->|true|ServerLastClientGoodMoveAckTime -->ShouldUsePackedMovementRPCs_Good bAckGoodMove -->|false|ServerLastClientAdjustmentTime -->ShouldUsePackedMovementRPCs_NoGood ShouldUsePackedMovementRPCs_Good -->|false|ClientAckGoodMove ShouldUsePackedMovementRPCs_NoGood -->|false|ClientAdjustPosition ShouldUsePackedMovementRPCs_Good -->|true|ServerSendMoveResponse ShouldUsePackedMovementRPCs_NoGood -->|true|ServerSendMoveResponse ServerSendMoveResponse -->MoveResponsePacked_ServerSend -->ClientMoveResponsePacked -->|Client|MoveResponsePacked_ClientReceive
同步时候,也向 Client 进行 SendClientAdjust,通知 Client 每次 NewMove 的结果;
根据 ShouldUsePackedMovemtnRPCs 决定是否需要 ServerSendMoveResponse;
flowchart LR MoveResponsePacked_ClientReceive -->ClientHandleMoveResponse -->IsGoodMove IsGoodMove -->|true|ClientAckGoodMove_Implementation IsGoodMove -->|false|ClientAdjustPosition_Implementation -->SetbUpdatePosition_true TickComponent -->ClientUpdatePositionAfterServerUpdate
Client 收到 DS 的 SendClientAdjust 后,判定 MoveResponse 是否 IsGoodMove,如果是,Client 进行 Ack,被确认的 Move 将会立刻从 SavedMoves 中移除;否则 Client 需要更新 bUpdatePosition 为 true,后续在 ClientUpdatePositionAfterServerUpdate 中进行修正;
ClientUpdatePositionAfterServerUpdate:判定 bUpdatePosition 是否是 true,如果是则回放 DS 未 Ack 的 ClientData->SavedMoves.Num(),进行 SetCurrentReplayedSavedMove 并 MoveFor;
SimulateProxy
flowchart LR ACharacter::OnRep_ReplicatedMovement -->AActor::PostNetReceiveVelocity ACharacter::OnRep_ReplicatedMovement -->ACharacter::PostNetReceiveLocationAndRotation AActor::PostNetReceiveVelocity -->UPrimitiveComponent::SetPhysicsLinearVelocity ACharacter::PostNetReceiveLocationAndRotation -->SmoothCorrection ACharacter::PostNetReceiveLocationAndRotation -->SetbNetworkUpdateReceived_true
数据同步后,Client 通过 OnRep_ReplicatedBasedMovement、OnRep_ReplicatedMovement 将坐标设置给 Actor;
flowchart LR TickComponent -->SimulatedTick -->SimulateMovement SimulatedTick -->|!bNetworkSmoothingComplete|SmoothClientPosition %% ----------- SimulateMovement -->ScopedUpdates ScopedUpdates -->bIsSimulatedProxy bIsSimulatedProxy -->bNetworkUpdateReceived_true bNetworkUpdateReceived_true -->|bNetworkGravityDirectionChanged|SetGravityDirection bNetworkUpdateReceived_true -->|bNetworkMovementModeChanged| ApplyNetworkMovementMode bNetworkUpdateReceived_true -->|bJustTeleported OR bForceNextFloorCheck|UpdateFloorFromAdjustment bNetworkUpdateReceived_false -->|bForceNextFloorCheck|UpdateFloorFromAdjustment %% ----------- ScopedUpdates -->UpdateCharacterStateBeforeMovement ScopedUpdates -->MaybeUpdateBasedMovement ScopedUpdates -->UpdateProxyAcceleration ScopedUpdates -->|!bHandledNetUpdate OR !bNetworkSkipProxyPredictionOnNetUpdate|MoveSmooth -->IsMovingOnGround IsMovingOnGround -->|true|MoveAlongFloor IsMovingOnGround -->|false|SafeMoveUpdatedComponent -->|!bSteppedUp|SlideAlongSurface ScopedUpdates -->UpdateCharacterStateAfterMovement ScopedUpdates -->OnMovementUpdated SimulateMovement -->CallMovementUpdateDelegate SimulateMovement -->UpdateComponentVelocity %% ----------- SmoothClientPosition -->SmoothClientPosition_Interpolate SmoothClientPosition -->SmoothClientPosition_UpdateVisuals
Smooth:
SmoothingServerTimeStamp 表示 Character 在 DS 当前移动时间戳,由 ACharacter::PreReplication 时,同步的 ReplicatedServerLastTransformUpdateTimeStamp 得来;
SmoothingClientTimeStamp 表示 Character 在这个 Client 当前平滑到的移动时间戳;
每次进行 SmoothClientPosition_Interpolate 时;
在 SmoothingMode = ENetworkSmoothingMode::Linear 的情况下:
- 计算
TargetDelta = LastCorrectionDelta,这里的LastCorrectionDelta = 上一次( SmoothingServerTimeStamp - SmoothingClientTimeStamp),表示实际上相比于DS上的数据,Client在这一次平滑开始前,还剩余多少时间还未执行平滑操作; - 更新
SmoothingClientTimeStamp = Min(SmoothingClientTimeStamp + DeltaSeconds, SmoothingServerTimeStamp + MaxTimeAhead);
这里的DeltaSeconds表示当帧过去的实际时间;
MaxTimeAhead = TargetDelta * 0.15f,表示允许多往前外插的时间,0.15f是允许多预测的时间比例;
现在这个新的SmoothingClientTimeStamp,就表示这一帧Client需要平滑到的时间戳; - 计算
RemainingTime = SmoothingServerTimeStamp - SmoothingClientTimeStamp, 表示在这一帧平滑过后,还剩下多少时间没有平滑;然后CurrentSmoothTime = TargetDelta - RemainingTime,得到这一帧需要平滑多少时间; - 计算
LerpPercent = FMath::Clamp(CurrentSmoothTime / TargetDelta, 0.0f, LerpLimit),按照本次平滑多少时间 / 剩余的总共需要平滑的时间,得到这个LerpPercent;
其中LerpLimit = 1.15f,也是允许多平滑的比例; - 得到
LerpPercent后,更新MeshTranslationOffset、MeshRotationOffset;
特别地,如果不是通过 DS 模式来进行同步(比如自定义协议),可以打包 FRepMovement 数据,然后手动进行 Replicate 模拟:
1 | CharacterActor->PreNetReceive(); |
参考
UE 5.4 源码
大体框架:UE4 移动的网络同步


